package com.intermancer.predictor.store.simple;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.intermancer.predictor.organism.Organism;
import com.intermancer.predictor.store.StoreEntry;
import com.intermancer.predictor.store.StoreEntryDAO;
import com.intermancer.predictor.system.InitializationException;

public class StoreEntryInMemoryDAO implements StoreEntryDAO {

	private class ScoreIndexEntry implements Comparable<ScoreIndexEntry> {
		public double score;
		public int indexIntoEntries;

		public ScoreIndexEntry() {
			// no-arg constructor because I need one.
		}
		
		@Override
		public boolean equals(Object obj) {
			if(obj instanceof ScoreIndexEntry) {
				return indexIntoEntries == ((ScoreIndexEntry) obj).indexIntoEntries;
			}
			return false;
		}

		@Override
		public int compareTo(ScoreIndexEntry o) {
			return this.indexIntoEntries - o.indexIntoEntries;
		}

		@Override
		public int hashCode() {
			return indexIntoEntries;
		}

	}

	private int maxSize;
	private List<StoreEntry> entries;
	private Map<Long, Integer> identityIndex;
	private List<Long> sortedIdentities;
	private List<ScoreIndexEntry> scoreIndex;
	
	private long nextIdentity = 0;
	private Map<Integer, List<Organism>> entriesByOrganismHashCode;
	
	public StoreEntryInMemoryDAO() {
		entriesByOrganismHashCode = new HashMap<Integer, List<Organism>>();
	}
	
	public StoreEntryInMemoryDAO(int maxSize) {
		this();
		setMaxSize(maxSize);
	}

	@Override
	public void delete(Long identity) {
		int index = identityIndex.get(identity).intValue();
		entries.remove(index);

		identityIndex.remove(identity);
		for (Entry<Long, Integer> entry : identityIndex.entrySet()) {
			if (entry.getValue() > index) {
				identityIndex.put(entry.getKey(), new Integer(
						entry.getValue() - 1));
			}
		}

		sortedIdentities.remove(identity);
		ScoreIndexEntry indexEntry = new ScoreIndexEntry();
		indexEntry.indexIntoEntries = index;
		scoreIndex.remove(indexEntry);
		for(ScoreIndexEntry entry : scoreIndex) {
			if(entry.indexIntoEntries > index) {
				entry.indexIntoEntries--;
			}
		}
	}

	@Override
	public StoreEntry get(Long identity) {
		Integer entriesIndex = identityIndex.get(identity);
		if (entriesIndex == null) {
			return null;
		}
		return entries.get(entriesIndex.intValue());
	}

	@Override
	public List<StoreEntry> get(List<Long> identities) {
		List<StoreEntry> entriesForReturn = new ArrayList<StoreEntry>();
		for (Long identity : identities) {
			StoreEntry entry = get(identity);
			if (entry != null) {
				entriesForReturn.add(entry);
			}
		}
		return entriesForReturn;
	}

	@Override
	public StoreEntry getByScoreIndex(int index) {
		if(entries.size() == 0) {
			return null;
		}
		if(index <= 0) {
			return entries.get(scoreIndex.get(0).indexIntoEntries);
		}
		if(index > entries.size() - 2) {
			return entries.get(scoreIndex.get(scoreIndex.size() - 1).indexIntoEntries);
		}
		return entries.get(scoreIndex.get(index).indexIntoEntries);
	}

	@Override
	public List<Long> getIdentities() {
		return sortedIdentities;
	}

	@Override
	public int getScoreIndex(double score) {
		int scoreIndexOffset = 0;
		for (ScoreIndexEntry scoreIndexEntry : scoreIndex) {
			if (scoreIndexEntry.score > score) {
				return scoreIndexOffset;
			} else {
				scoreIndexOffset++;
			}
		}
		if (entries.size() < maxSize) {
			return scoreIndexOffset;
		} else {
			return entries.size();
		}
	}

	@Override
	public int getMaxSize() {
		return maxSize;
	}

	public void setMaxSize(int maxSize) {
		this.maxSize = maxSize;
		if (maxSize == 0) {
			throw new InitializationException(
					"The StoreEntryInMemoryDAO.maxSize cannot be 0");
		}
		entries = new ArrayList<StoreEntry>(maxSize);
		identityIndex = new HashMap<Long, Integer>(maxSize);
		scoreIndex = new ArrayList<ScoreIndexEntry>(maxSize);
		sortedIdentities = new ArrayList<Long>(maxSize);
	}

	@Override
	public StoreEntry put(StoreEntry storeEntry) {
		if (entries.size() >= maxSize) {
			return null;
		}
		StoreEntry completedStoreEntry = new StoreEntry(storeEntry);
		if (duplicateEntry(completedStoreEntry)) {
			completedStoreEntry.setIdentity(null);
			return completedStoreEntry;
		}
		addStoreEntry(completedStoreEntry);

		return completedStoreEntry;
	}

	private void addStoreEntry(StoreEntry storeEntry) {
		storeEntry.setIdentity(getNextIdentity());
		entries.add(storeEntry);
		int entriesIndex = entries.size() - 1;
		identityIndex.put(storeEntry.getIdentity(), entriesIndex);
		sortedIdentities.add(storeEntry.getIdentity());

		updateScoreIndex(storeEntry, entriesIndex);
		updateOrganismHashList(storeEntry);
	}

	private void updateScoreIndex(StoreEntry storeEntry, int entriesIndex) {
		ScoreIndexEntry scoreIndexEntry = new ScoreIndexEntry();
		scoreIndexEntry.score = storeEntry.getScore();
		scoreIndexEntry.indexIntoEntries = entriesIndex;
		int placeInScoreIndex = 0;
		for (ScoreIndexEntry entry : scoreIndex) {
			if (entry.score <= storeEntry.getScore()) {
				placeInScoreIndex++;
			} else {
				break;
			}
		}
		scoreIndex.add(placeInScoreIndex, scoreIndexEntry);

	}

	private void updateOrganismHashList(StoreEntry storeEntry) {
		Integer hashCode = new Integer(storeEntry.getOrganism().hashCode());
		List<Organism> list = entriesByOrganismHashCode.get(hashCode);
		if (list == null) {
			list = new ArrayList<Organism>();
			entriesByOrganismHashCode.put(hashCode, list);
		}
		list.add(storeEntry.getOrganism());
	}

	public Long getNextIdentity() {
		Long identity = new Long(nextIdentity);
		nextIdentity++;
		return identity;
	}

	@Override
	public int getSize() {
		return entries.size();
	}
	
	private boolean duplicateEntry(StoreEntry storeEntry) {
		Integer organismHashCode = new Integer(storeEntry.getOrganism().hashCode());
		List<Organism> organismsWithSameHashCode = entriesByOrganismHashCode.get(organismHashCode);
		if (organismsWithSameHashCode != null) {
			return organismsWithSameHashCode.contains(storeEntry.getOrganism());
		}
		return false;
	}

}
